<?php

namespace app\admin\controller\ai\index;

use app\BaseController;
use think\App;

class Index extends BaseController
{
    private const DOU_BAO_HOST = 'ark.cn-beijing.volces.com';
    private const DOU_BAO_PATH = '/api/v3/chat/completions';
    private const DOU_BAO_API_KEY = '0892b9df-9fd2-4c87-bfcd-bd40a2efe9ca';
    private const DOU_BAO_SCHEME = 'https';
    private const DOU_BAO_PORT = 443;
    private array $dnsFallbackServers = ['223.5.5.5', '119.29.29.29', '8.8.8.8'];

    public function __construct(App $app)
    {
        parent::__construct($app);
    }

    /**
     * 豆包AI对话接口
     * @return bool|string
     */
    public function chat(): bool|string
    {
        try {
            $param = $this->request->param();
            $this->validate($param, [
                'message|消息内容' => 'require',
            ]);

            $message = (string)$param['message'];
            // $temperature = isset($param['temperature']) ? (float)$param['temperature'] : 0.7;
            // $top_p = isset($param['top_p']) ? (float)$param['top_p'] : 0.9;
            // $max_tokens = isset($param['max_tokens']) ? (int)$param['max_tokens'] : 1024;

            // 构造请求体（Volcengine Ark Chat Completions）
            $requestBody = [
                'model' => 'doubao-1-5-thinking-pro-250415',
                'messages' => [
                    [
                        'role' => 'user',
                        'content' => $message,
                    ]
                ],
//                'temperature' => $temperature,
//                'top_p' => $top_p,
//                'max_tokens' => $max_tokens,
                'stream' => false,
            ];

            $result = $this->requestDoubao($requestBody);

            // 解析响应
            $reply = '';
            if (isset($result['choices'][0]['message']['content'])) {
                $reply = (string)$result['choices'][0]['message']['content'];
            } elseif (isset($result['choices'][0]['delta']['content'])) {
                $reply = (string)$result['choices'][0]['delta']['content'];
            }

            if ($reply === '') {
                // 如果提供了错误信息则透传
                if (isset($result['error'])) {
                    $msg = is_array($result['error']) ? json_encode($result['error'], JSON_UNESCAPED_UNICODE) : (string)$result['error'];
                    throw new \Exception('豆包返回异常：' . $msg);
                }
                throw new \Exception('豆包未返回内容');
            }

            return success([
                'response' => $reply,
                'model' => $requestBody['model'],
                'usage' => $result['usage'] ?? [],
                'timestamp' => time(),
            ], 'AI回复成功');
        } catch (\Exception $e) {
            return error($e->getMessage(), $e->getCode(), [
                'line' => $e->getLine(),
                'file' => $e->getFile(),
            ]);
        }
    }

    /**
     * 豆包AI流式对话接口
     * @return bool|string
     */
    public function chatStream(): bool|string
    {
        try {
            $param = $this->request->param();
            $this->validate($param, [
                'message|消息内容' => 'require',
            ]);

            $message = (string)$param['message'];
            // $temperature = isset($param['temperature']) ? (float)$param['temperature'] : 0.7;
            // $top_p = isset($param['top_p']) ? (float)$param['top_p'] : 0.9;
            // $max_tokens = isset($param['max_tokens']) ? (int)$param['max_tokens'] : 1024;

            $requestBody = [
                'model' => 'doubao-1-5-thinking-pro-250415',
                'messages' => [
                    [
                        'role' => 'user',
                        'content' => $message,
                    ]
                ],
                // 'temperature' => $temperature,
                // 'top_p' => $top_p,
                // 'max_tokens' => $max_tokens,
                'stream' => true,
            ];

            $result = $this->requestDoubaoStream($requestBody);

            if ($result['content'] === '' && empty($result['chunks'])) {
                throw new \Exception('豆包未返回任何流式内容');
            }

            return success([
                'response' => $result['content'],
                'chunks' => $result['chunks'],
                'model' => $requestBody['model'],
                'usage' => $result['usage'],
                'finish_reason' => $result['finish_reason'],
                'timestamp' => time(),
            ], 'AI流式回复成功');
        } catch (\Exception $e) {
            return error($e->getMessage(), $e->getCode(), [
                'line' => $e->getLine(),
                'file' => $e->getFile(),
            ]);
        }
    }

    /**
     * 调用火山方舟（豆包）Chat Completions
     * @param array $body
     * @return array
     * @throws \Exception
     */
    private function requestDoubao(array $body): array
    {
        if ($this->canUseSwooleHttpClient()) {
            return $this->requestDoubaoBySwoole($body);
        }

        return $this->requestDoubaoByCurl($body);
    }

    /**
     * 是否可以使用 Swoole 协程 HttpClient
     * @return bool
     */
    private function canUseSwooleHttpClient(): bool
    {
        if (!class_exists('\Swoole\Coroutine')) {
            return false;
        }

        if (!class_exists('\Swoole\Coroutine\Http\Client')) {
            return false;
        }

        // getCid 为 -1 表示不在协程环境
        return \Swoole\Coroutine::getCid() > 0;
    }

    /**
     * 使用 Swoole 协程客户端请求
     * @param array $body
     * @return array
     * @throws \Exception
     */
    private function requestDoubaoBySwoole(array $body): array
    {
        $host = self::DOU_BAO_HOST;
        $path = self::DOU_BAO_PATH;
        $apiKey = self::DOU_BAO_API_KEY;

        $ip = $this->resolveHost($host);
        $payload = json_encode($body, JSON_UNESCAPED_UNICODE);

        /** @var \Swoole\Coroutine\Http\Client $client */
        $client = new \Swoole\Coroutine\Http\Client($ip, self::DOU_BAO_PORT, true);
        $client->set([
            'timeout' => 60,
            'connect_timeout' => 10,
            'dns_cache_expire' => 0,
            'ssl_host_name' => $host,
        ]);
        $client->setMethod('POST');
        $client->setData($payload);
        $client->setHeaders([
            'Content-Type' => 'application/json',
            'Authorization' => 'Bearer ' . $apiKey,
            'Accept' => 'application/json',
            'Host' => $host,
        ]);

        $client->execute($path);


        if ($client->errCode !== 0) {
            $errMsg = method_exists($client, 'errMsg') ? $client->errMsg : swoole_strerror($client->errCode);
            $client->close();
            throw new \Exception('请求豆包失败：' . $errMsg, $client->errCode);
        }

        $statusCode = (int)$client->getStatusCode();
        $bodyString = (string)$client->getBody();
        $client->close();

        if ($statusCode < 200 || $statusCode >= 300) {
            throw new \Exception('豆包HTTP状态码异常：' . $statusCode . '，响应：' . $bodyString);
        }

        $data = json_decode($bodyString, true);
        if (json_last_error() !== JSON_ERROR_NONE) {
            throw new \Exception('解析豆包响应失败：' . json_last_error_msg());
        }

        return $data;
    }

    /**
     * 使用 curl 请求
     * @param array $body
     * @return array
     * @throws \Exception
     */
    private function requestDoubaoByCurl(array $body): array
    {
        $url = self::DOU_BAO_SCHEME . '://' . self::DOU_BAO_HOST . self::DOU_BAO_PATH;
        $apiKey = self::DOU_BAO_API_KEY;

        $resolvedIp = $this->resolveHost(self::DOU_BAO_HOST);

        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body, JSON_UNESCAPED_UNICODE));
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
        curl_setopt($ch, CURLOPT_TIMEOUT, 60);
        curl_setopt($ch, CURLOPT_DNS_CACHE_TIMEOUT, 0);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
        curl_setopt($ch, CURLOPT_RESOLVE, [
            self::DOU_BAO_HOST . ':' . self::DOU_BAO_PORT . ':' . $resolvedIp,
        ]);
        curl_setopt($ch, CURLOPT_HTTPHEADER, [
            'Content-Type: application/json',
            'Authorization: Bearer ' . $apiKey,
        ]);

        $response = curl_exec($ch);
        $errno = curl_errno($ch);
        $error = curl_error($ch);
        $httpCode = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        if ($errno) {
            throw new \Exception('请求豆包失败：' . $error, $errno);
        }

        if ($httpCode < 200 || $httpCode >= 300) {
            throw new \Exception('豆包HTTP状态码异常：' . $httpCode . '，响应：' . $response);
        }

        $data = json_decode((string)$response, true);
        if (json_last_error() !== JSON_ERROR_NONE) {
            throw new \Exception('解析豆包响应失败：' . json_last_error_msg());
        }

        return $data;
    }

    /**
     * 调用豆包流式接口，汇总增量内容
     * @param array $body
     * @return array
     * @throws \Exception
     */
    private function requestDoubaoStream(array $body): array
    {
        if ($this->inSwooleCoroutine()) {
            return $this->requestDoubaoStreamBySwoole($body);
        }

        $url = self::DOU_BAO_SCHEME . '://' . self::DOU_BAO_HOST . self::DOU_BAO_PATH;
        $apiKey = self::DOU_BAO_API_KEY;

        $resolvedIp = $this->resolveHost(self::DOU_BAO_HOST);

        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body, JSON_UNESCAPED_UNICODE));
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
        curl_setopt($ch, CURLOPT_HEADER, false);
        curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
        curl_setopt($ch, CURLOPT_TIMEOUT, 180);
        curl_setopt($ch, CURLOPT_DNS_CACHE_TIMEOUT, 0);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
        curl_setopt($ch, CURLOPT_HTTPHEADER, [
            'Content-Type: application/json',
            'Authorization: Bearer ' . $apiKey,
            'Accept: text/event-stream',
            'Cache-Control: no-cache',
            'Connection: keep-alive',
        ]);
        curl_setopt($ch, CURLOPT_RESOLVE, [
            self::DOU_BAO_HOST . ':' . self::DOU_BAO_PORT . ':' . $resolvedIp,
        ]);
        if (!$this->inSwooleCoroutine()) {
            if (defined('CURLOPT_TCP_KEEPALIVE')) {
                curl_setopt($ch, CURLOPT_TCP_KEEPALIVE, 1);
            }
            if (defined('CURLOPT_TCP_KEEPIDLE')) {
                curl_setopt($ch, CURLOPT_TCP_KEEPIDLE, 30);
            }
            if (defined('CURLOPT_TCP_KEEPINTVL')) {
                curl_setopt($ch, CURLOPT_TCP_KEEPINTVL, 10);
            }
        }
        curl_setopt($ch, CURLOPT_FAILONERROR, false);

        $buffer = '';
        $content = '';
        $chunks = [];
        $usage = [];
        $finishReason = null;

        curl_setopt($ch, CURLOPT_WRITEFUNCTION, function ($curl, $data) use (&$buffer, &$content, &$chunks, &$usage, &$finishReason) {
            $this->processStreamBuffer($buffer, $data, $content, $chunks, $usage, $finishReason);
            return strlen($data);
        });

        curl_exec($ch);
        $errno = curl_errno($ch);
        $error = curl_error($ch);
        $httpCode = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        if (in_array($errno, [CURLE_PARTIAL_FILE, CURLE_RECV_ERROR], true) && $httpCode === 200) {
            // 某些环境下服务端主动断开连接作为结束信号，容错处理
            $errno = 0;
        }

        if ($errno) {
            throw new \Exception('请求豆包失败：' . $error, $errno);
        }

        if ($httpCode < 200 || $httpCode >= 300) {
            throw new \Exception('豆包HTTP状态码异常：' . $httpCode);
        }

        $this->flushStreamBuffer($buffer, $content, $chunks, $usage, $finishReason);

        if ($content === '' && empty($chunks)) {
            throw new \Exception('豆包未返回任何流式数据');
        }

        return [
            'content' => $content,
            'chunks' => $chunks,
            'usage' => $usage,
            'finish_reason' => $finishReason,
        ];
    }

    /**
     * 使用 Swoole 协程客户端请求豆包流式接口
     * @param array $body
     * @return array
     * @throws \Exception
     */
    private function requestDoubaoStreamBySwoole(array $body): array
    {
        $host = self::DOU_BAO_HOST;
        $ip = $this->resolveHost($host);

        $client = new \Swoole\Coroutine\Http\Client($ip, self::DOU_BAO_PORT, true);
        $client->set([
            'timeout' => 180,
            'connect_timeout' => 10,
            'keep_alive' => true,
            'ssl_host_name' => $host,
            'package_max_length' => 2 * 1024 * 1024,
        ]);
        $client->setMethod('POST');
        $client->setData(json_encode($body, JSON_UNESCAPED_UNICODE));
        $client->setHeaders([
            'Host' => $host,
            'Content-Type' => 'application/json',
            'Authorization' => 'Bearer ' . self::DOU_BAO_API_KEY,
            'Accept' => 'text/event-stream',
            'Cache-Control' => 'no-cache',
            'Connection' => 'keep-alive',
        ]);

        $result = $client->execute(self::DOU_BAO_PATH);
        if ($result === false) {
            $errMsg = method_exists($client, 'errMsg') ? $client->errMsg : swoole_strerror($client->errCode);
            $client->close();
            throw new \Exception('请求豆包失败：' . $errMsg, $client->errCode);
        }

        if ($client->statusCode < 200 || $client->statusCode >= 300) {
            $bodyString = (string)$client->body;
            $client->close();
            throw new \Exception('豆包HTTP状态码异常：' . $client->statusCode . '，响应：' . $bodyString);
        }

        $buffer = '';
        $content = '';
        $chunks = [];
        $usage = [];
        $finishReason = null;

        // execute 已经拿到首段数据
        if (!empty($client->body)) {
            $this->processStreamBuffer($buffer, $client->body, $content, $chunks, $usage, $finishReason);
        }

        while (true) {
            $chunk = $client->recv();
            if ($chunk === '' || $chunk === null) {
                break;
            }
            if ($chunk === false) {
                if (in_array($client->errCode, [0, SOCKET_ECONNRESET, SOCKET_EPIPE], true)) {
                    break;
                }
                $errMsg = method_exists($client, 'errMsg') ? $client->errMsg : swoole_strerror($client->errCode);
                $client->close();
                throw new \Exception('请求豆包失败：' . $errMsg, $client->errCode);
            }
            $this->processStreamBuffer($buffer, $chunk, $content, $chunks, $usage, $finishReason);
        }

        $client->close();

        $this->flushStreamBuffer($buffer, $content, $chunks, $usage, $finishReason);

        if ($content === '' && empty($chunks)) {
            throw new \Exception('豆包未返回任何流式数据');
        }

        return [
            'content' => $content,
            'chunks' => $chunks,
            'usage' => $usage,
            'finish_reason' => $finishReason,
        ];
    }

    /**
     * 主动解析域名，兼容 Swoole/原生环境
     * @param string $host
     * @param int $timeout
     * @return string
     * @throws \Exception
     */
    private function resolveHost(string $host, int $timeout = 3): string
    {
        $ip = '';

        if (class_exists('\Swoole\Coroutine') && \Swoole\Coroutine::getCid() > 0) {
            // 优先使用 Swoole 协程解析，尝试多个 DNS
            foreach ($this->dnsFallbackServers as $dnsServer) {
                \Swoole\Coroutine::set([
                    'dns_server' => $dnsServer,
                    'dns_cache_expire' => 0,
                ]);
                try {
                    $ip = \Swoole\Coroutine\System::dnsLookup($host, $timeout);
                } catch (\Throwable $throwable) {
                    $ip = '';
                }
                if ($ip && filter_var($ip, FILTER_VALIDATE_IP)) {
                    break;
                }
                $ip = '';
            }
        }

        if (!$ip) {
            $ip = gethostbyname($host);
            if ($ip === $host) {
                $ip = '';
            }
        }

        if (!$ip) {
            throw new \Exception('DNS解析失败：' . $host);
        }

        return $ip;
    }

    /**
     * 查找流式分隔符位置
     * @param string $buffer
     * @return array|false
     */
    private function getDelimiterPosition(string $buffer): array|false
    {
        $position = strpos($buffer, "\n\n");
        if ($position !== false) {
            return ['position' => $position, 'length' => 2];
        }
        $position = strpos($buffer, "\r\n\r\n");
        if ($position !== false) {
            return ['position' => $position, 'length' => 4];
        }
        return false;
    }

    /**
     * 处理流式数据缓冲区
     * @param string $buffer
     * @param string $data
     * @param string $content
     * @param array $chunks
     * @param array $usage
     * @param string|null $finishReason
     * @return void
     */
    private function processStreamBuffer(string &$buffer, string $data, string &$content, array &$chunks, array &$usage, ?string &$finishReason): void
    {
        $buffer .= $data;

        while (true) {
            $delimiterPosition = $this->getDelimiterPosition($buffer);
            if ($delimiterPosition === false) {
                break;
            }

            $packet = substr($buffer, 0, $delimiterPosition['position']);
            $buffer = substr($buffer, $delimiterPosition['position'] + $delimiterPosition['length']);
            $packet = trim($packet);
            if ($packet === '') {
                continue;
            }

            $lines = preg_split("/\r?\n/", $packet);
            foreach ($lines as $line) {
                $line = trim($line);
                if ($line === '') {
                    continue;
                }
                if (!str_starts_with($line, 'data:')) {
                    continue;
                }
                $payload = trim(substr($line, 5));
                if ($payload === '[DONE]') {
                    $chunks[] = [
                        'type' => 'done',
                        'raw' => $payload,
                    ];
                    continue;
                }

                $json = json_decode($payload, true);
                if (!is_array($json)) {
                    $chunks[] = [
                        'type' => 'unknown',
                        'raw' => $payload,
                    ];
                    continue;
                }

                $deltaContent = '';
                if (isset($json['choices'][0])) {
                    $choice = $json['choices'][0];
                    if (isset($choice['delta']['content'])) {
                        $deltaContent = (string)$choice['delta']['content'];
                        $content .= $deltaContent;
                    }
                    if (isset($choice['finish_reason']) && $choice['finish_reason']) {
                        $finishReason = $choice['finish_reason'];
                    }
                }

                if (isset($json['usage'])) {
                    $usage = $json['usage'];
                }

                $chunks[] = [
                    'type' => $json['event'] ?? 'chunk',
                    'delta' => $deltaContent,
                    'raw' => $json,
                ];
            }
        }
    }

    /**
     * 清空剩余缓冲区，避免遗漏最后一段数据
     * @param string $buffer
     * @param string $content
     * @param array $chunks
     * @param array $usage
     * @param string|null $finishReason
     * @return void
     */
    private function flushStreamBuffer(string &$buffer, string &$content, array &$chunks, array &$usage, ?string &$finishReason): void
    {
        $remaining = trim($buffer);
        $buffer = '';
        if ($remaining === '') {
            return;
        }

        $lines = preg_split("/\r?\n/", $remaining);
        foreach ($lines as $line) {
            $line = trim($line);
            if ($line === '' || !str_starts_with($line, 'data:')) {
                continue;
            }
            $payload = trim(substr($line, 5));
            if ($payload === '[DONE]') {
                $chunks[] = [
                    'type' => 'done',
                    'raw' => $payload,
                ];
                continue;
            }
            $json = json_decode($payload, true);
            if (!is_array($json)) {
                $chunks[] = [
                    'type' => 'unknown',
                    'raw' => $payload,
                ];
                continue;
            }
            $deltaContent = '';
            if (isset($json['choices'][0])) {
                $choice = $json['choices'][0];
                if (isset($choice['delta']['content'])) {
                    $deltaContent = (string)$choice['delta']['content'];
                    $content .= $deltaContent;
                }
                if (isset($choice['finish_reason']) && $choice['finish_reason']) {
                    $finishReason = $choice['finish_reason'];
                }
            }
            if (isset($json['usage'])) {
                $usage = $json['usage'];
            }
            $chunks[] = [
                'type' => $json['event'] ?? 'chunk',
                'delta' => $deltaContent,
                'raw' => $json,
            ];
        }
    }

    /**
     * 判断是否处于 Swoole 协程环境，避免设置不被支持的 curl 选项
     * @return bool
     */
    private function inSwooleCoroutine(): bool
    {
        return class_exists('\Swoole\Coroutine') && \Swoole\Coroutine::getCid() > 0;
    }
}